package com.littlerpc.common.util;

import org.objectweb.asm.ClassReader;

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Stream;


/**
 * Created by ChengPeng on 2017/6/25.
 */
public class WebScanner {

    private static final String ERROR_MESSAGE = "packageName can't be null";
    private static final String URL_PROTOCOL_FILE = "file";
    private static final String URL_PROTOCOL_JAR = "jar";
    private static final String CLASS_FILE_SUFFIX = ".class";
    private static final String PATH_SEPARATOR = "/";
    private static final String PACKAGE_SEPARATOR = ".";

    /**
     * 扫描packages下面符合filter的class
     */
    public static Set<Class<?>> scanPackages(String[] packages, ClassFilter filter) {
        checkPackagesName(packages);
        //用于保存class的集合
        Set<Class<?>> classList = new LinkedHashSet<>();

        for (String packageName : packages) {
            scanPackage(packageName, filter, classList);
        }
        return classList;
    }

    /**
     * 扫描包
     */
    private static void scanPackage(String packageName, ClassFilter filter, Set<Class<?>> classList) {
        ClassLoader cl = ClassUtils.getClassLoader();
        Enumeration<URL> resources = null;
        try {
            resources = cl.getResources(convertClassNameToResourcePath(packageName));
        } catch (IOException e) {
            e.printStackTrace();
        }
        while (resources.hasMoreElements()) {
            URL url = resources.nextElement();
            converUrl(url, packageName,filter, classList);
        }
    }

    /**
     * 根据url的类型 进行不同的操作
     * @param url 文件的url
     * @param packageName 扫描的包名
     * @param filter 选择class的条件
     * @param classList 保存匹配的class
     */
    private static void converUrl(URL url, String packageName, ClassFilter filter, Set<Class<?>> classList) {
        ClassLoader classLoader = ClassUtils.getClassLoader();
        if (isFileURL(url)) {
            String file = url.getFile();
            doScanFile(new File(file), packageName,filter,classList,classLoader);
        } else if (isJarURL(url)) {
            doScanJar(url, packageName, filter, classList,classLoader);
        }
    }

    /**
     * 扫描jar里面的文件
     * @param url
     * @param packageName jar的包名
     * @param filter 匹配的条件
     * @param classList 匹配的class集合
     * @param classLoader
     */
    private static void doScanJar(URL url, String packageName, ClassFilter filter, Set<Class<?>> classList,ClassLoader classLoader) {

        try {
            URLConnection con = url.openConnection();
            if (con instanceof JarURLConnection) {
                JarURLConnection jarCon = (JarURLConnection) con;

                JarFile jarFile = jarCon.getJarFile();

                Enumeration<JarEntry> entries = jarFile.entries();

                while (entries.hasMoreElements()){
                    JarEntry jarEntry = entries.nextElement();

                    String name = convertResourcePathToClassName(jarEntry.getName());
                    if(isClass(name) && checkClassName(name,packageName)){
                        InputStream is = classLoader.getResourceAsStream(jarEntry.getName());
                        addClass(packageName, filter, classList, classLoader, is);
                    }
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 将符合条件的类添加到集合中
     * @param packageName
     * @param filter
     * @param classList
     * @param classLoader
     * @param is
     */
    private static void addClass(String packageName, ClassFilter filter, Set<Class<?>> classList, ClassLoader classLoader, InputStream is) {
        ClassReader classReader = null;
        try {
            classReader = new ClassReader(is);
        } catch (IOException e) {
            e.printStackTrace();
        }
        String className = convertResourcePathToClassName(classReader.getClassName());
        if (checkClassName(className, packageName)) {
            String cla = className ;
            //添加class
            Class<?> aClass = null;
            try {
                if(filter.accept( aClass =   classLoader.loadClass(cla)/*Class.forName(cla, false, classLoader)*/)) {

                    classList.add(aClass);
                }
            } catch (ClassNotFoundException e) {
                try {
                    throw e;
                } catch (ClassNotFoundException e1) {
                    //ingonire
                }
            }finally {
                try {
                    is.close();
                } catch (IOException e) {
                    //ingonire
                }
            }
        }
    }

    /**
     * 扫描文件夹里面符合条件的类
     * @param file
     * @param packageName
     * @param filter
     * @param classList
     * @param classLoader
     */
    private static void doScanFile(File file, String packageName, ClassFilter filter, Set<Class<?>> classList,ClassLoader classLoader) {
        if (isDirectory(file)) {
            Stream.of(file.listFiles()).forEach(f -> doScanFile(f, packageName,filter, classList,classLoader));
        } else if (isClass(file.getAbsolutePath())) {
            try {
                InputStream is = new FileInputStream(file);
                addClass(packageName, filter, classList, classLoader, is);
            } catch (IOException  e) {
                e.printStackTrace();
            }
        }


    }


    public static boolean isFileURL(URL url) {
        String protocol = url.getProtocol();
        return URL_PROTOCOL_FILE.equals(protocol);
    }

    /**
     * Determine whether the given URL points to a resource in a jar file,
     * that is, has protocol "jar", "zip", "vfszip" or "wsjar".
     *
     * @param url the URL to check
     * @return whether the URL has been identified as a JAR URL
     */
    public static boolean isJarURL(URL url) {
        String protocol = url.getProtocol();
        return URL_PROTOCOL_JAR.equals(protocol);
    }


    /**
     * 检查是否符合条件
     * @param className
     * @param packageName
     * @return
     */
    private static boolean checkClassName(String className, String packageName) {
        String[] classPathArr = className.split(PACKAGE_SEPARATOR);
        String[] packArr = packageName.split(PACKAGE_SEPARATOR);

        int packIndex = 0;
        int pathIndex = 0;
        while (packIndex < packArr.length && pathIndex < classPathArr.length) {
            if("class".equals(classPathArr[pathIndex])) {
                return true;
            }
            if (!packArr[packIndex].equals(classPathArr[pathIndex])) {
                return false;
            }
            packIndex++;
            pathIndex++;
        }

        return true;

    }


    private static boolean isClass(String file) {
        return file.endsWith(CLASS_FILE_SUFFIX);
    }

    private static boolean isJarFile(String file) {
        return file.endsWith(URL_PROTOCOL_JAR);
    }

    private static boolean isDirectory(File file) {
        return file.isDirectory();
    }


    private static void checkPackagesName(String[] packages) {
        if (packages == null || packages.length == 0){
            throw new NullPointerException(ERROR_MESSAGE);
        }

    }


    /**
     * Convert a "/"-based resource path to a "."-based fully qualified class name.
     *
     * @param resourcePath the resource path pointing to a class
     * @return the corresponding fully qualified class name
     */
    public static String convertResourcePathToClassName(String resourcePath) {
        int first = -1;
        if ((first = resourcePath.indexOf(PATH_SEPARATOR)) != -1) {
            if (first == 0) {
                resourcePath = resourcePath.substring(1);
            }
        }

        return resourcePath.replace(PATH_SEPARATOR, PACKAGE_SEPARATOR);
    }

    /**
     * Convert a "."-based fully qualified class name to a "/"-based resource path.
     *
     * @param className the fully qualified class name
     * @return the corresponding resource path, pointing to the class
     */
    public static String convertClassNameToResourcePath(String className) {
        return className.replace(PACKAGE_SEPARATOR, PATH_SEPARATOR);
    }

}
